/*
 * 
 * iFinity Software Development - http://www.ifinity.com.au
 * Copyright (c) 2006 - 2009
 
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 
 * documentation files (the "Software"), the rights to use, copy, modify, merge, publish, distribute, sublicense
 * copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the 
 * following conditions:

 * 1. The above copyright notice and this permission notice shall be included in all copies or substantial portions 
 *    of the Software.  
 * 2. The software may not be claimed as the original work and may not be sold as a stand alone product

 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 
 * TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 
 * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
 * DEALINGS IN THE SOFTWARE.
 * 
 */

using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Web;
using System.Text;
using System.Text.RegularExpressions;
using System.IO;
using System.Xml;
using System.Configuration.Provider;

/* dotnetnuke references */
using DotNetNuke.Common;
using DotNetNuke.Common.Utilities;
using DotNetNuke.Entities.Portals;
using DotNetNuke.Entities.Tabs;
using DotNetNuke.Entities.Modules;
using DotNetNuke.Entities.Modules.Definitions;
using DotNetNuke.Services.Localization;
using DotNetNuke.Framework.Providers;

namespace DotNetNuke.Providers.SearchEngineSiteMapProvider
{
    /// <summary>
    /// The SiteMapProvider class provides a simple base class of a Search Engine Site map generator.
    /// The class is designed to be inherited with a different derivation for each different type of DotNetNuke Module
    /// </summary>
    public class SiteMapProvider 
    {
        protected string _name="";
        protected double _defaultPagePriority = 0.5;
        protected string _defaultPageUpdateFrequency = "never";
        protected string _sitePageTypes = "";
        protected bool _subPathsInMap = true;
        protected string _siteURL = "";
        protected string _baseSiteURL = "";
        protected string _portalAliasURL="";
        protected bool _friendlyURLs = false;
        protected bool _includeHiddenPages = false;
        protected bool _includeDisabledPages = false;
        protected int _maxEntriesPerSitemap = 50000;
        private bool _useCachedSitemap = false;
        private int _sitemapCacheMinutes = 0;
        private bool _useSitemapIndex = false;
        protected string _adminPagesRegex = "/Admin/";
        protected string _excludePages ="";
        protected List<int> _excludedTabs;
        private bool _debug = false;
        private int _dbgMsg = 0;
        protected PortalSettings _portalSettings;
        #region Public Properties
        public string Name
        {
            get { return _name; }
            set { _name = value; }
        }
        public bool Debug
        {
            get { return _debug; }
            set { _debug = value; }
        }
        public double DefaultPagePriority
        {
            get { return _defaultPagePriority; }
            set {_defaultPagePriority = value;}
        }
        public ChangeFrequencies DefaultPageUpdateFrequency
        {
            get { return SitePage.ConvertChangeFrequencyToEnum(_defaultPageUpdateFrequency); }
            set { _defaultPageUpdateFrequency = SitePage.ConvertChangeFrequencyToString(value); }
        }
        public string SitePageTypes
        {
            get { return _sitePageTypes; }
            set { _sitePageTypes = value; }
        }
        public bool SubPathsInMap
        {
            get { return _subPathsInMap; }
            set { _subPathsInMap = value; }
        }
        protected bool UseCachedSitemap
        {
            get { return _useCachedSitemap; }
            set { _useCachedSitemap = value; }
        }
        protected int SitemapCacheMinutes
        {
            get { return _sitemapCacheMinutes;}
            set {_sitemapCacheMinutes = value;}
        }
        protected bool UseSitemapIndex
        {
            get { return _useSitemapIndex; }
            set { _useSitemapIndex = value; }
        }


        public bool InitialisedOk
        {
            get
            {
                if (_siteURL != "" && _portalAliasURL != "" && _baseSiteURL != "")
                    return true;
                else
                    return false;
            }
        }
        #endregion

        //The Overridden Base Members section contains the methods overriding the Base Provider class
        #region Overridden Base Members
        public SiteMapProvider(string name, NameValueCollection attributes) 
        {
            Initialize(name, attributes);
        }
        /// <summary>
        /// Initialise sets the initial values of the Provider based on the information contained in the config file.
        /// </summary>
        /// <param name="name">The name of the Provider</param>
        /// <param name="config">The config section of the config file.</param>
        public void Initialize(string name, NameValueCollection config)
        {
            _name = name;
            if (config == null)
                throw new ArgumentNullException("config");

            //base.Initialize(name, config);

            if (config["defaultPagePriority"] != null)
            {
                //set the default page priority
                _defaultPagePriority = Convert.ToDouble(config["defaultPagePriority"]);
            }

            if (config["defaultPageUpdateFrequency"] != null)
            {
                //set the default page update frequency
                _defaultPageUpdateFrequency = config["defaultPageUpdateFrequency"];
            }
            if (config["includeHiddenPages"] != null)
            {
                _includeHiddenPages = Convert.ToBoolean(config["includeHiddenPages"]);
            }
            if (config["includeDisabledPages"] != null)
            {
                _includeDisabledPages = Convert.ToBoolean(config["includeDisabledPages"]);
            }

            if (config["useSitemapIndex"] != null)
            {
                if (!bool.TryParse(config["useSitemapIndex"], out _useSitemapIndex))
                    _useSitemapIndex = false;
            }
            //when using a sitemap index, sitemaps must be cached
            if (_useSitemapIndex)
            {
                _useCachedSitemap = true;
            }
            else
            {
                if (config["useCachedSitemap"] != null)
                {
                    if (!bool.TryParse(config["useCachedSitemap"], out _useCachedSitemap))
                        _useCachedSitemap = false;
                }
            }
            //get cache time for sitemap cache.
            if (_useCachedSitemap && config["sitemapCacheMinutes"] != null)
            {
                if (!int.TryParse(config["sitemapCacheMinutes"], out _sitemapCacheMinutes))
                    _sitemapCacheMinutes = 0;
            }

            //get max entries for sitemap
            if (config["maxEntriesPerSitemap"] != null)
            {
                int.TryParse(config["maxEntriesPerSitemap"], out _maxEntriesPerSitemap);
            }
            //633 : allow exclusion of named pages
            if (config["excludePages"] != null)
            {
                _excludePages = (string)config["excludePages"];
            }
            // exclude admin pages by regex pattern if necessary
            if (config["adminPagesRegex"] != null)
                _adminPagesRegex = (string)config["adminPagesRegex"];
        }
        /// <summary>
        /// Returns the description of the Provider
        /// </summary>
        public string Description
        {
            get
            {
                return "The Google Site Maps Provider returns a Google Sitemap XML Stream";
            }
        }
        #endregion

        // The Public Members region contains the publicly accessible members of the class
        #region Public Members
        /// <summary>
        /// The main procedure for the provider.  This procedure, when called, will return a string containing an XML document in the Google Sitemap 0.84 schema.
        /// </summary>
        /// <param name="siteURL">The site URL to index.</param>
        /// <returns>string value containing an XML document.</returns>
        public string GetGoogleSiteMap(string siteURL)
        {
            int sitemapNum = GetSiteMapNum();
            //set the base url and portal settings if not existing
            SetBaseURL(siteURL);
            if (_debug) DebugMessage("Identified Portal Alias :" + _portalAliasURL);
            if (_debug && _portalSettings != null) DebugMessage("Identified Portal : " + _portalSettings.PortalId);

            //determine sitemap or sitemap index
            if (_useSitemapIndex && sitemapNum == 0)
            {
                if (_debug) DebugMessage("Using Sitemap Index");
                //generate the sitemap index
                StringWriter stringWriter = new StringWriter();
                XmlTextWriter writer = new XmlTextWriter(stringWriter);
                WriteSitemapIndex(writer);
                return stringWriter.ToString();
            }
            else
            {
                //generate the sitemap
                if (GetNewSiteMap(sitemapNum))
                {
                    if (_useCachedSitemap)
                    {
                        if (_debug) DebugMessage("Generating new Cached Sitemap [" + sitemapNum.ToString() + "]");
                        //generate the sitemap as a file, then read it back
                        string sitemapFile = GetSitemapFilePath(sitemapNum);
                        XmlTextWriter writer = new XmlTextWriter(sitemapFile, Encoding.UTF8);
                        if (WriteSitemapPages(writer, sitemapNum))
                        {
                            writer.Close();
                            //now read it back out
                            string result = "";
                            using (StreamReader rdr = File.OpenText(sitemapFile))
                            {
                                result = rdr.ReadToEnd();
                            }
                            return result;
                        }
                        else
                        {
                            if (writer != null)
                            {
                                writer.Close();
                            }
                            //delete this file : invalid now
                            if (File.Exists(sitemapFile))
                                File.Delete(sitemapFile);
                            return "";
                        }
                    }
                    else
                    {
                        //return the sitemap as text 
                        if (_debug) DebugMessage("Using Realtime Sitemap Generation");
                        StringWriter stringWriter = new StringWriter();
                        XmlTextWriter writer = new XmlTextWriter(stringWriter);
                        if (WriteSitemapPages(writer, 1))
                        {
                            return stringWriter.ToString();
                        }
                        else
                        {
                            return "";
                        }
                    }
                }
                else
                    {
                    //read back the existing file
                    if (_debug) DebugMessage("Using Cached Sitemap [" + sitemapNum.ToString() + "]");
                    string sitemapFile = GetSitemapFilePath(sitemapNum);
                    string result = "";
                    using (StreamReader rdr = File.OpenText(sitemapFile))
                    {
                        result = rdr.ReadToEnd();
                    }
                    return result;
                }
            }
        }

        protected void DebugMessage(string message)
        {
            if (HttpContext.Current != null)
            {
                HttpResponse response = HttpContext.Current.Response;
                if (response != null)
                {
                    response.AppendHeader("x-Sitemap-Debug-" + _dbgMsg.ToString(), message);
                    _dbgMsg++;
                }
            }
        }

        #endregion
        #region private members
        protected int GetSiteMapNum()
        {
            int siteMapNum = 0;string rawSitemapNum =null;
            if (_useSitemapIndex == false)
                siteMapNum = 1;
            else
            {
                if (HttpContext.Current != null && HttpContext.Current.Request != null)
                {
                    rawSitemapNum = HttpContext.Current.Request["sitemap"];
                    if (rawSitemapNum != null)
                        int.TryParse(rawSitemapNum, out siteMapNum);
                }
            }
            return siteMapNum;
        }
        protected bool GetNewSiteMap(int sitemapNum)
        {
            bool result = true; // always expect to get new sitemap
            if (_useCachedSitemap)
            {
                //if we're using caching, then we check 
                string sitemapFilePath = GetSitemapFilePath(sitemapNum);
                if (File.Exists(sitemapFilePath))
                {
                    //check to see if it's an empty file
                    //a zero length file is a stub file
                    //any more than that, it's a real file, so check it for expiration
                    FileInfo fi = new FileInfo(sitemapFilePath);
                    if (fi.Length > 10)
                    {
                        //check to see if the expiry date has passed.
                        DateTime lastWrite = File.GetLastWriteTimeUtc(sitemapFilePath);
                        DateTime expiryDate = lastWrite.AddMinutes (_sitemapCacheMinutes);
                        //if the expiry date is stil in the future, don't get a new file
                        if (expiryDate > DateTime.UtcNow)
                        {
                            result = false;
                        }
                    }
                }
            }
            return result;
        }
        protected void WriteSitemapIndex(XmlTextWriter writer)
        {
            try
            {
                WriteSitemapIndexHeader(writer);
                //read through the sitemaps for this portal
                int numsitemaps = ReadExistingSitemaps();
                if (numsitemaps < 1)
                    numsitemaps = 1;//always at least one sitemap
                for (int sitemapNum = 1; sitemapNum <= numsitemaps; sitemapNum++)
                {
                    WriteSitemapIndexEntry(writer, sitemapNum);
                }
                WriteSiteMapIndexFooter(writer);
            }
            catch (Exception ex)
            {
                WriteExceptionComment(writer, ex);
                //write the footer and close.
                WriteSiteMapIndexFooter(writer);
            }
        }

        private int ReadExistingSitemaps()
        {
            int numberSitemaps = 0;
            if (_portalSettings != null)
            {
                string sitemapPath = GetSitemapPath();
                string[] sitemapFiles = Directory.GetFiles(sitemapPath, "sitemap*.xml", SearchOption.TopDirectoryOnly);
                numberSitemaps = sitemapFiles.GetUpperBound(0) + 1;//counts the number of sitemap files
            }
            return numberSitemaps;
        }
        private string GetSitemapPath()
        {
            string path = _portalSettings.HomeDirectoryMapPath;
            if (path.EndsWith("\\"))
            {
                path += "\\";
            }
            path += "sitemaps\\";

            if (Directory.Exists(path) == false)
            {
                Directory.CreateDirectory(path);
            }
            return path;
        }
        private string GetSitemapFilePath(int sitemapNum)
        {
            string sitemapFilePath = GetSitemapPath();

            if (_useSitemapIndex)
                sitemapFilePath += "sitemap" + sitemapNum.ToString() + ".xml";
            else
                sitemapFilePath += "sitemap.xml";

            return sitemapFilePath;
        }
        private string GetSitemapUrl(int sitemapNum)
        {
            string url = _baseSiteURL;
            string scheme = "http://";
            if (url.EndsWith("/") == false)
                url += "/";

            if (HttpContext.Current != null && HttpContext.Current.Request != null)
            {
                //gets whole http://domain.com/path/googlesitemap.axd url
                string completeUrl = HttpContext.Current.Request.Url.AbsoluteUri.ToLower(); 
                scheme = HttpContext.Current.Request.Url.Scheme + System.Uri.SchemeDelimiter;
                string portalAlias = _portalAliasURL.ToLower();//613 : mixed case portal alias can cause incorrect urls
                int startingPoint = completeUrl.IndexOf(portalAlias);
                int finishingPoint = startingPoint + portalAlias.Length;
                int queryStart = completeUrl.IndexOf("?");
                if (queryStart > finishingPoint)
                {
                    //take existing qurey string off
                    url = completeUrl.Substring(finishingPoint, queryStart - finishingPoint);
                }
                else
                    url = completeUrl.Substring(finishingPoint);

                //add on the query string value
                url += "?sitemap=" + sitemapNum.ToString();
            }
            else
            {
                url += "googlesitemap.axd?sitemap=" + sitemapNum.ToString();
            }
            if (url.StartsWith("/") == false)
                url = "/" + url;
            //put the portal alias url on the front
            url = scheme + _portalAliasURL + url;
            return url;
        }
        
        /// <summary>
        /// Checks for the next sitemap in the index, and creates a stub if there isn't one
        /// </summary>
        /// <param name="writer"></param>
        private void CheckForNextSitemap(int nextSitemapNum)
        {
            string filePath = GetSitemapFilePath(nextSitemapNum);
            if (!File.Exists(filePath))
            {
                //create stub text file
                using (StreamWriter sw = File.CreateText(filePath))
                { 
                    sw.Close(); 
                }
            }
        }
        protected bool WriteSitemapPages(XmlTextWriter writer, int sitemapNum)
        {
            bool outputSomething = false;
            //instantiate the XML Text Writer for writing the SiteMap document
            try
            {
                //write out the header
                WriteSiteMapHeader(writer);

                int startingUrl = ((sitemapNum - 1) * _maxEntriesPerSitemap) + 1;
                int endingUrl = (sitemapNum) * _maxEntriesPerSitemap;
                if (_debug) DebugMessage("Starting Url:" + startingUrl.ToString() + ", Ending Url:" + endingUrl.ToString());
                //get the list of site pages
                List<SitePage> sitePages = GetSitePages(sitemapNum, startingUrl , endingUrl);
                if (_debug) DebugMessage(sitePages.Count.ToString() + " sitePages returned");
                //for each page, write out the XML 
                int urlCount = 0;
                foreach (SitePage sitePage in sitePages)
                {
                    urlCount++;
                    if (urlCount >= startingUrl)
                    {
                        //call down to get the page entry
                        if (urlCount <= endingUrl)
                        {
                            outputSomething = true;
                            WriteSitemapUrlEntry(writer, sitePage);
                        }
                        else 
                        {
                            if (_useSitemapIndex == false)
                            {
                                WriteURLCountExceeded(writer);
                                break;
                            }
                            else
                            {
                                //using sitemap index : therefore using cached items
                                //check for next sitemap, create stub if not there
                                CheckForNextSitemap(sitemapNum + 1);
                                break;
                            }
                        }
                    }
                }
                //writer.Close();
                //write the footer and close.
                WriteSiteMapFooter(writer, sitemapNum, urlCount, startingUrl , endingUrl);
            }
            catch (Exception ex)
            {
                WriteExceptionComment(writer, ex);
                //write the footer and close.
                WriteSiteMapFooter(writer);
                outputSomething = true;
                if (_debug) DebugMessage("WriteSiteMapPages Exception: " + ex.Message);
            }
            return outputSomething;
        }

        
        #endregion

        // The Internal Members region contains code which is meant to be used within the provider.
        #region Internal Members
        protected bool GetSafeSetting(System.Collections.Specialized.NameValueCollection config, string attributeName, bool defaultValue)
        {
            bool result = defaultValue;
            string raw = config[attributeName];
            if (!bool.TryParse(raw, out result))
                result = defaultValue; //explicitly set, because tryparse will return false if can't be parsed
            return result;
        }
        protected int GetSafeSetting(System.Collections.Specialized.NameValueCollection config, string attributeName, int defaultValue)
        {
            int result = defaultValue;
            string raw = config[attributeName];
            if (string.IsNullOrEmpty(raw) == false)
                int.TryParse(raw, out result);
            return result;
        }
        protected double GetSafeSetting(System.Collections.Specialized.NameValueCollection config, string attributeName, double defaultValue)
        {
            double result = defaultValue;
            string raw = config[attributeName];
            double.TryParse(raw, out result);
            return result;
        }
        protected string GetSafeSetting(System.Collections.Specialized.NameValueCollection config, string attributeName, string defaultValue)
        {
            string result = defaultValue;
            string raw = config[attributeName];
            if (raw != null)
                result = raw;
            return result;
        }


        /// <summary>
        /// Takes the siteURL (which may contain the SiteMaps handler resource) and strips it back to the base site URL.
        /// </summary>
        /// <param name="siteURL"></param>
        protected internal void SetBaseURL(string siteURL)
        {
            _friendlyURLs = (DotNetNuke.Entities.Host.HostSettings.GetHostSetting("UseFriendlyUrls") == "Y");
            _siteURL = siteURL;
            int httpEnd = siteURL.IndexOf("http://") + "http://".Length;
            if (siteURL.Contains("http://") == false)
                httpEnd = 0;
            
            _baseSiteURL = siteURL;
            //strip the port number out if it is localhost
            if (siteURL.IndexOf(":", httpEnd + 1) > 0 && siteURL.IndexOf("localhost") > 0)
            {
                //there is a port qualifier in it
                //work forwards from the http end until the first "/"
                string port = "";
                int portStart, portEnd;
                portStart = siteURL.IndexOf(":", httpEnd + 1);
                portEnd = siteURL.IndexOf("/", httpEnd + 1);
                port = siteURL.Substring(portStart, portEnd - portStart);
                _baseSiteURL = _baseSiteURL.Replace(port, "");
            }


            PortalAliasInfo alias = GetPortalAlias(_baseSiteURL);
            if (alias == null)
            {
                //get the first alias for the host portal as a fall back?
                //this code should not run - but it's here to save disaster
                string hostPortalRaw = DotNetNuke.Entities.Host.HostSettings.GetHostSetting("HostPortalId");
                int hostPortalId = 0;
                if (Int32.TryParse(hostPortalRaw, out hostPortalId))
                {
                    PortalAliasController pca = new PortalAliasController();
                    ArrayList aliases = pca.GetPortalAliasArrayByPortalID(hostPortalId);
                    if (aliases.Count > 0)
                        alias = (PortalAliasInfo)aliases[0];
                }
            }
            if (alias != null)
            {
                _baseSiteURL = "http://" + alias.HTTPAlias;
                _portalAliasURL = alias.HTTPAlias;
                //from 4.8.1 onwards, portalSettings might be null when we get to this point, so 
                //look it up and set it for the remainder of processing
                _portalSettings = PortalController.GetCurrentPortalSettings();
                if (_portalSettings == null)
                {
                    _portalSettings = new PortalSettings(-1, alias);
                    HttpApplication app = HttpContext.Current.ApplicationInstance;
                    if (app != null)
                        app.Context.Items.Add("PortalSettings", _portalSettings);
                }
            }
            /*DotNetNuke.Entities.Portals.PortalAliasCollection portalAliases = PortalSettings.GetPortalAliasLookup();
            foreach (string key in portalAliases.Keys)
            {
                //get the first match the URL front and the portal alias
                if (_baseSiteURL.IndexOf(portalAliases[key].HTTPAlias, httpEnd) > -1)
                {
                    //this one will do - close enough!
                    _baseSiteURL = "http://" + portalAliases[key].HTTPAlias;
                    //from 4.8.1 onwards, portalSettings might be null when we get to this point, so 
                    //look it up and set it for the remainder of processing
                    _portalSettings = PortalController.GetCurrentPortalSettings();
                    if (_portalSettings == null)
                    {
                        _portalSettings = new PortalSettings(-1, portalAliases[key]);
                        HttpApplication app = HttpContext.Current.ApplicationInstance ;
                        if (app != null)
                            app.Context.Items.Add("PortalSettings", _portalSettings);
                    }
                    break;
                }
            }
            //get the portalAlias URL - which is the base site URL minus the http://
            _portalAliasURL = _baseSiteURL.Substring(httpEnd);
             * */
            if (_portalAliasURL.Substring(_portalAliasURL.Length - 1) == "/")
            {
                _portalAliasURL = _portalAliasURL.Substring(0,_portalAliasURL.Length - 1); 
            }
            

        }
        /// <summary>
        /// Returns a list of sitepages
        /// </summary>
        /// <param name="startingIndex">The first url to start from</param>
        /// <param name="endingIndex">The last url to count to</param>
        /// <returns></returns>
        private List<SitePage> GetSitePages(int sitemapNum, int startingIndex, int endingIndex)
        {
            int urlCount = 0;
            if (InitialisedOk == false) return new List<SitePage>();   //jump out if we are here because a provider didn't override this method
            //otherwise, generate a list of pages for the site
            string roles;
            List<SitePage> pageList = new List<SitePage>();
            PortalAliasInfo portalAlias = PortalSettings.GetPortalAliasInfo(_portalAliasURL);
            int portalId = portalAlias.PortalID;

            GenerateExcludedTabList(portalId);

            //get the total list of providers for this application
            SearchEngineSitemapProviderCol providers = SearchEngineSitemapService.Providers;

            //get all the portal pages 
            TabController tabController = new TabController();
            ArrayList tabs = tabController.GetTabs(portalId);
            LocaleCollection locales = Localization.GetEnabledLocales();
            bool useLanguageInUrl = Localization.UseLanguageInUrl();
            if (_debug) DebugMessage(tabs.Count.ToString() + " tabs found for portal");
            if (_debug) DebugMessage("Number of locales:" + locales.Count.ToString());
            foreach (TabInfo ti in tabs)
            {
                //DebugMessage("checking tab: " + ti.TabPath + "[id:" + ti.TabID.ToString() + "]");
                bool outputTab = true; string reason = null;
                //633 : allow exclusion of tabs by name
                if (_excludedTabs.Contains(ti.TabID))
                    outputTab = false; reason = "excluded";

                if (outputTab)
                {
                    //exclude the admin, host and deleted tabs, and tabs that are not showing yet or tabs that have expired
                    outputTab = !ti.IsAdminTab && !ti.IsSuperTab && !ti.IsDeleted;
                    if (!outputTab) reason = "admin, superuser or deleted tab [isAdminTab:" + ti.IsAdminTab.ToString() + ", isSuperTab:" + ti.IsSuperTab.ToString() + ", isDeleted:" + ti.IsDeleted.ToString() + "]";
                }

                if (outputTab)
                {
                    //not expired or not yet enabled
                    outputTab = !(ti.EndDate != DateTime.MinValue && ti.EndDate < DateTime.Now.Date) && !(ti.StartDate > DateTime.Now.Date);
                    if (!outputTab) reason = "expired or not yet enabled";
                }

                if (outputTab)
                {
                    //check if admin tab for DNN 5+ where IsAdminTab doesn't work anymore
                    outputTab = !(Regex.IsMatch(ti.TabPath, _adminPagesRegex)); 
                    if (!outputTab) reason = "Admin Tab (via admin regex [" + _adminPagesRegex + "])";
                }

                if (outputTab)
                {
                    //check url type tabs, because they could be an externally linked page
                    if (ti.TabType == TabType.Url)
                    {
                        //check that not an external link
                        if (!ti.Url.StartsWith(_baseSiteURL))
                            outputTab = false; reason = "external url";
                    }
                }

                if (outputTab)
                {
                    roles = ti.AuthorizedRoles;
                    //only select public accessible pages
                    //781 : include 'unauthenticated users'
                    if (roles.IndexOf("All Users") > -1 || roles.IndexOf("Unauthenticated Users") > -1)
                    {
                        outputTab = true;
                    }
                    else
                    {
                        reason = "no permission for anon or unauthenticated users";
                        outputTab = false; //614 set to false when all users not found
                    }
                }
                if (outputTab)
                {
                    //693 : portals with no locales will not work correctly in prior locale loop.
                    //process locales into a list.  Allows use of a loop, even if one of the locales is null
                    List<Locale> processLocales = new List<Locale>();
                    if (locales.Count == 0)
                    {
                        //no locales, add a dummy locale
                        processLocales.Add(null);
                    }
                    else
                    {
                        foreach (Locale locale in locales.AllValues)
                        {
                            processLocales.Add(locale);
                        }
                    }
                    //now process the locales we have identified
                    foreach (Locale targetLocale in processLocales)
                    {
                        string url;
                        //if no locale, or only one locale, or ignoring the language
                        if (targetLocale == null || processLocales.Count == 1 || !useLanguageInUrl)
                            url = GetTabUrl(ti);//get the URL of the tab
                        else
                        {
                            url = GetTabUrl(ti, targetLocale.Code);
                            //targetLocale = locale;
                        }
                        //855 : check to make sure all urls belong to the correct domain before adding
                        bool belongsToDomain =BelongsToDomain(_baseSiteURL, url); 
                        if (url.Length > 0 && belongsToDomain)  //if no url, no tab to put in list
                        {
                            //exclude hidden and disabled pages
                            if ((ti.IsVisible || (ti.IsVisible == false && _includeHiddenPages == true))
                             && (ti.DisableLink == false || (ti.DisableLink == true && _includeDisabledPages == true)))
                            {
                                //add the url to the list
                                urlCount++;//we are going to count this url
                                double pagePriority = GetTabPriority(_defaultPagePriority, ti);//635 : use dnn page priority if available
                                pageList.Add(new SitePage(url, DateTime.Now, SitePage.ConvertChangeFrequencyToEnum(_defaultPageUpdateFrequency), pagePriority));
                            }
                            else
                            {
                                if (_debug) DebugMessage(ti.TabPath + " [id:" + ti.TabID.ToString() + "] not output. Reason : not visible/disabledlink");
                            }

                            //get any extra module-specific page URL's  - it is up to the implementing provider whether to
                            //include hidden pages
                            List<SitePage> extraPages = GetModuleSpecificPages(portalAlias, ti, targetLocale, providers, sitemapNum, urlCount, startingIndex, endingIndex);
                            if (extraPages.Count > 0)
                            {
                                //and add them onto the list 
                                urlCount = urlCount + extraPages.Count;
                                pageList.AddRange(extraPages);
                            }
                            if (urlCount > endingIndex)
                                break;
                        }
                        else
                        {
                            if (_debug)
                            {
                                if (belongsToDomain == false)
                                    DebugMessage(ti.TabPath + " [id:" + ti.TabID.ToString() + "] not output. Reason : Url [" + url + "] does not belong to domain [" + _baseSiteURL + "]");
                                else
                                    DebugMessage(ti.TabPath + " [id:" + ti.TabID.ToString() + "] not output. Reason : Url Length == 0 Url[" + url + "]");
                            }
                        }
                        if (useLanguageInUrl == false)
                            break;//from local iteration, because we aren't putting locales into the urls
                    }
                }
                else
                {
                    if (reason == null) reason = "unknown";
                    if (_debug) DebugMessage(ti.TabPath + " [id:" + ti.TabID.ToString() + "] not output. Reason:" + reason);
                }
                if (urlCount > endingIndex)
                    break;//don't get any more tabs if the output is full
            }
            return pageList;
        }
        /// <summary>
        /// Checks that the supplied URl belongs to the supplied domain
        /// </summary>
        /// <param name="domainUrl">Domain including the http:// start</param>
        /// <param name="url">Fully qualified Url</param>
        /// <returns></returns>
        protected bool BelongsToDomain(string domainUrl, string url)
        {
            bool belongsToDomain = false;
            if (url.ToLower().StartsWith(domainUrl.ToLower()))
                belongsToDomain = true;

            return belongsToDomain;
        }

        public double GetTabPriority(double defaultPriority, TabInfo ti)
        {
            double result = Convert.ToDouble(ti.SiteMapPriority);
            return System.Math.Round(result, 2);
        }

        /// <summary>
        /// The SitePagesForTab method is designed to be overriden by deriving DotNetNuke Google Sitemap Provider classes.  This method
        /// should be overriden to provide module-specific implementation of the site map logic.  When writing an overridden version
        /// of this procedure, the overriding procedure should return any different Url's used to access module content on the supplied Tab.
        /// </summary>
        /// <remarks>This method is used to produce all of the variations of page Urls a DotNetNuke module can create.  This is normally due 
        /// to query strings (or, friendly urls). You shouldn't write overriden versions of both SitePages/SitePagesForTab methods unless 
        /// they return unique sets of SitePage objects.  Note that it is up to the module provider to return a set of items within 
        /// the specified index bounds.  Any items returned greater than the number of places left (up to the 'endingCount') will be 
        /// discarded for the called sitemap.  It is the responsibility of the sitemap provider to hold state on which urls have been 
        /// returned and which have not been included in this range.</remarks>
        /// <param name="portalAlias">The portal alias being indexed</param>
        /// <param name="tab">The current tab being indexed.</param>
        /// <param name="locale">The current locale being used.  Will be null if no locale specified</param>
        /// <param name="sitemapNum">The current sitemap number in the index file. 0 means no sitemap index being used.</param>
        /// <param name="urlCount">The current number of urls already added to the sitemap</param>
        /// <param name="startingCount">The index number of the first url added to the sitemap</param>
        /// <param name="endingCount">The index number of the last url that will be added to the sitemap</param>
        /// <returns>A typed List of SitePage obejcts, one instance per unique URL to be included in the site map.</returns>
        
        public virtual List<SitePage> SitePagesForTab(PortalAliasInfo portalAlias, TabInfo tab,Locale locale,int sitemapNum, int urlCount, int startingCount, int endingCount)
        {
            return new List<SitePage>();
        }
        
        /// <summary>
        /// Writes out a standard sitemaps 0.9 schema sitemap index header
        /// </summary>
        internal void WriteSitemapIndexHeader(XmlTextWriter writer)
        {
            writer.WriteProcessingInstruction("xml", "version=\"1.0\" encoding=\"UTF-8\"");
            writer.WriteStartElement("sitemapindex");
            writer.WriteAttributeString("xmlns", "http://www.sitemaps.org/schemas/sitemap/0.9");
        }
        /// <summary>
        /// Writes out the Xml for the sitemap index file, for the specified sitemap number
        /// </summary>
        /// <param name="writer"></param>
        /// <param name="sitemapNum"></param>
        protected void WriteSitemapIndexEntry(XmlTextWriter writer, int sitemapNum)
        {
            string sitemapFilePath = GetSitemapFilePath(sitemapNum);
            string sitemapUrl = GetSitemapUrl(sitemapNum);
            DateTime sitemapDate = DateTime.Now;
            if (File.Exists(sitemapFilePath))
            {
                sitemapDate = File.GetLastWriteTimeUtc(sitemapFilePath);
            }

            writer.WriteStartElement("sitemap");
            byte[] locBytes;
            locBytes = Encoding.UTF8.GetBytes(sitemapUrl);
            writer.WriteElementString("loc", Encoding.UTF8.GetString(locBytes));
            writer.WriteElementString("lastmod", SitePage.FormatISODate(sitemapDate));
            writer.WriteEndElement();
        }
        /// <summary>
        /// Writes out a standard sitemaps 0.9 schema sitemap index footer
        /// </summary>
        /// <param name="writer"></param>
        internal void WriteSiteMapIndexFooter(XmlTextWriter writer)
        {
            writer.WriteEndElement();
        }
        /// <summary>
        /// This method writes out the standard 0.9 schema Google Sitemap header.
        /// </summary>
        /// <param name="writer">An instance of an XMLWriter being used to construct the Sitemap XML document.</param>
        internal void WriteSiteMapHeader(XmlWriter writer)
        {
            //start off the site map
            writer.WriteProcessingInstruction("xml", "version=\"1.0\" encoding=\"UTF-8\"");
            writer.WriteStartElement("urlset");
            writer.WriteAttributeString("xmlns", "http://www.sitemaps.org/schemas/sitemap/0.9");
        }
        /// <summary>
        /// This methods writes out the standard 0.84 schema Google Sitemap header.
        /// </summary>
        /// <param name="writer">An instance of an XMLWriter being used to construct the Sitemap XML document.</param>
        internal void WriteSiteMapFooter(XmlWriter writer)
        {
            writer.WriteEndElement();
        }
        internal void WriteSiteMapFooter(XmlWriter writer,int sitemapNum,int urlCount,int startingUrl ,int endingUrl)
        {
            int count = urlCount - startingUrl;
            int range = startingUrl + count;
            if (count > 0) count++; // add one if there is more than zero.
            string commentText= "Sitemap {0} contains {1} urls, range is from url {2} to url {3}";
            commentText = string.Format(commentText, sitemapNum, count, startingUrl, range);
            writer.WriteComment(commentText);
            writer.WriteEndElement();
        }
        /// <summary>
        /// Transforms the SitePage object into the 0.84 schema XML string and writes it into the Sitemap XML document.
        /// </summary>
        /// <param name="writer">An instance of an XMLWriter being used to construct the Sitemap XML document.</param>
        internal void WriteSitemapUrlEntry(XmlWriter writer, SitePage sitePage)
        {
            
            if (sitePage.GetType() == typeof(SitePageException))
            {
                writer.WriteComment(sitePage.Comment);
            }
            else
            {
                writer.WriteStartElement("url");
                byte[] locBytes;
                locBytes = Encoding.UTF8.GetBytes(sitePage.Loc);
                writer.WriteElementString("loc", Encoding.UTF8.GetString(locBytes));
                writer.WriteElementString("lastmod", SitePage.FormatISODate(sitePage.LastMod));
                writer.WriteElementString("changefreq", sitePage.ChangeFreq.ToString());
                //785 : culture-insensitive priority output
                writer.WriteElementString("priority", sitePage.Priority.ToString("0.0#"));
                if (sitePage.Comment != null && sitePage.Comment.Length > 0)
                    writer.WriteComment(sitePage.Comment);
                writer.WriteEndElement();
            }
            
        }
        /// <summary>
        /// Writes in a comment into the XML File if the number of URL's in the Sitemap document has exceeded the maximum settings.
        /// </summary>
        /// <param name="writer">An instance of an XMLWriter being used to construct the Sitemap XML document.</param>
        internal void WriteURLCountExceeded(XmlWriter writer)
        {
            writer.WriteComment("The URL Count has reached " + _maxEntriesPerSitemap.ToString() + " . Your sitemap settings restrict your Url output to this number.  Either increase the maximum entries (cannot exceed 50,000 per sitemap) or create a sitemap index file by including the useSitemapIndex='true' attribute in your configuration.");
        }
        internal void WriteExceptionComment(XmlWriter writer, Exception ex)
        {
            writer.WriteComment("An Exception has occured : " + ex.Message + " - trace: " + ex.StackTrace);
        }
        internal List<SitePage> IteratePages()
        {
            return new List<SitePage>();
		}
        private void GenerateExcludedTabList(int portalId)
        {
            _excludedTabs = new List<int>();
            if (_excludePages != null && _excludePages.Length > 0)
            {
                string[] rawValues = _excludePages.Split(';');
                foreach (string rawval in rawValues)
                {
                    //if it is numeric, add straight to the list
                    int tabId = -1;
                    if (int.TryParse(rawval, out tabId))
                        _excludedTabs.Add(tabId);
                    else
                    {
                        //tab named, so look up by name.
                        TabController tc = new TabController();
                        TabInfo excludedTab = tc.GetTabByName(rawval, portalId);
                        if (excludedTab != null)
                        {
                            _excludedTabs.Add(excludedTab.TabID);
                        }
                    }
                }
            }
        }
        /// <summary>
        /// This section iterates the different modules on a page, to get the complete set of possible URL's for the page
        /// </summary>
        /// <param name="portalAlias">An instance of the portalAlias object for the current site</param>
        /// <param name="ti">An instance of the TabInfo object representing the current page</param>
        /// <param name="locale">The current locale to generate the url for.  Note, locale may be null</param>
        private List<SitePage> GetModuleSpecificPages(PortalAliasInfo portalAlias, TabInfo ti, Locale locale, SearchEngineSitemapProviderCol providers, int sitemapNum, int urlCount, int startingCount, int endingCount)
        {
            List<SitePage> allPages = new List<SitePage>();
                ModuleController modCtl = new ModuleController();
                ModuleDefinitionController modDefs = new ModuleDefinitionController();
                Hashtable processedModules = new Hashtable();
                ti.Modules = modCtl.GetPortalTabModules(portalAlias.PortalID, ti.TabID);
                if (ti.Modules != null)
                {
                    foreach (ModuleInfo mod in ti.Modules)
                    {
                        List<SitePage> modulePages = new List<SitePage>();
                        //get the module type for the module
                        ModuleDefinitionInfo modDef = modDefs.GetModuleDefinition(mod.ModuleDefID);
                        //construct the name to search for the provider under
                        //search under old and new names, in case someone copied the old definition over
                        string moduleProviderNameOld = modDef.FriendlyName + ".GoogleSiteMapProvider";
                        string moduleProviderNameNew = modDef.FriendlyName + ".SearchEngineSiteMapProvider";
                        //check to see if this module was already processed for this page
                        if (processedModules.ContainsKey(moduleProviderNameOld) == false && processedModules.ContainsKey(moduleProviderNameNew) == false)
                        {
                            //see if there is a provider for this specific module
                            SiteMapProvider provider = (SiteMapProvider)providers.FindByName(moduleProviderNameNew);
                            string moduleProviderName = null;
                            if (provider == null)
                            {
                                provider = (SiteMapProvider)providers.FindByName(moduleProviderNameOld);
                                moduleProviderName = moduleProviderNameOld;
                            }
                            else
                            {
                                moduleProviderName = moduleProviderNameNew;
                            }

                            if (provider != null)
                            {
                                //set provider friendly url and tab priority values from current tab
                                string localeDebug = ""; string debugComment = "Module Provider '{0}' generated {1} results for locale '{2}'";
                                provider.Debug = _debug;
                                //sand box the provider so that an exception in one provider doesn't take out entire sitemap
                                try
                                {
                                    //a provider was found so call it for for the list of pages in it
                                    modulePages = provider.SitePagesForTab(portalAlias, ti, locale, sitemapNum, urlCount, startingCount, endingCount);
                                    if (_debug && modulePages.Count > 0)
                                        modulePages[0].Comment = string.Format(debugComment, moduleProviderName, modulePages.Count, localeDebug);

                                }
                                catch (Exception ex)
                                {
                                    //error - most likely in the called module provider 
                                    string message = ex.Message;
                                    if (_debug)
                                        message += "\n" + ex.StackTrace;
                                    modulePages.Add(new SitePage("http://" + _portalAliasURL, DateTime.Now, ChangeFrequencies.never, 0, "Exception in Provider [" + provider.Name + "] " + message));
                                }

                                //keep a copy of the pages to fill the dictionary
                                processedModules.Add(moduleProviderName, modulePages);
                                //increase the url count (kept locally)
                                urlCount += modulePages.Count;
                                if (modulePages.Count > 1)
                                {
                                    modulePages[0].Comment += " First Entry from module provider '" + moduleProviderName + "'";
                                    modulePages[modulePages.Count - 1].Comment += " Last entry from module provider '" + moduleProviderName + "' [" + modulePages.Count.ToString() + " entries in total]";
                                }
                                //keep the list of pages
                                if (modulePages != null && allPages != null)
                                    allPages.AddRange(modulePages);

                                if (startingCount + urlCount > endingCount)
                                    break; //got more than we are going to output in the sitemap, so stop collecting
                            }
                        }
                    }
                }
            return allPages;
        }

        /// <summary>
        /// GetFiles returns the files in a given directory path, for a given string filter.  This works in a recursive pattern.
        /// </summary>
        /// <param name="path">The path to search in.</param>
        /// <param name="filter"></param>
        /// <returns>An IEnumerable list of strings, representing the pathLength.</returns>
        internal static IEnumerable<string> GetFiles(string path, string filter)
        {
            foreach (string s in Directory.GetFiles(path, filter))
            {
                yield return s;
            }
            foreach (string s in Directory.GetDirectories(path))
            {
                foreach (string s1 in GetFiles(s, filter))
                {
                    yield return s1;
                }
            }
        }
        protected virtual PortalAliasInfo GetPortalAlias(string requestUrl)
        {
            PortalAliasInfo alias = null;
            PortalAliasController pac = new PortalAliasController();
            OrderedDictionary portalRegexes = GetPortalAliasRegexes();
            foreach (string regexPattern in portalRegexes.Keys)
            {
                //split out the portal alias from the regex pattern representing that alias
                if (Regex.IsMatch(requestUrl, regexPattern, RegexOptions.IgnoreCase))
                {
                    alias = (PortalAliasInfo)portalRegexes[regexPattern];
                    break;
                }
            }
            return alias;
        }
        protected ChangeFrequencies GetChangeFrequencyFromLastChange(TimeSpan timeSinceLastChange)
        {
            ChangeFrequencies changeFreq = ChangeFrequencies.never;
            if (timeSinceLastChange.Days < 3)
            {
                if (timeSinceLastChange.Hours < 2)
                    changeFreq = ChangeFrequencies.hourly;
                else
                    changeFreq = ChangeFrequencies.daily;
            }
            else
                if (timeSinceLastChange.Days  > 3 && timeSinceLastChange.Days < 10)
                    changeFreq = ChangeFrequencies.weekly;
                else
                    if (timeSinceLastChange.Days  > 10 && timeSinceLastChange.Days < 40)
                        changeFreq = ChangeFrequencies.monthly;
                    else
                        if (timeSinceLastChange.Days < 400)
                            changeFreq = ChangeFrequencies.yearly;
                        else
                            changeFreq = ChangeFrequencies.never;

            return changeFreq;
        }
        /// <summary>
        /// Returns an ordered dictionary of alias regex patterns.  These patterns are used to identify a portal alias by getting a match.
        /// </summary>
        /// <remarks>711 : replace logic to always return child aliases first before parent aliases</remarks>
        /// <returns></returns>
        protected OrderedDictionary GetPortalAliasRegexes()
        {
            //get all aliases for this DNN install
            PortalAliasController pac = new PortalAliasController();
            PortalAliasCollection aliases = pac.GetPortalAliases();
            //create a new OrderedDictionary.  We use this because we
            //want to key by the correct regex pattern and return the
            //portalAlias that matches, and we want to preserve the
            //order of the items, such that the item with the most path separators (/)
            //is at the front of the list.  
            OrderedDictionary regexList = new OrderedDictionary(aliases.Count);
            //this regex pattern, when formatted with the httpAlias, will match a request 
            //for this portalAlias
            string aliasRegexPattern = @"(?:^(?<http>http[s]{0,1}://){0,1})(?<alias>_ALIAS_(?<path>$|/[\w]*))";
            List<int> pathLengths = new List<int>();
            foreach (string aliasKey in aliases.Keys)
            {
                PortalAliasInfo alias = aliases[aliasKey];
                //regex escape the portal alias for inclusion into a regex pattern
                string plainAlias = alias.HTTPAlias;
                string escapedAlias = Regex.Escape(plainAlias);
                //format up the regex pattern by replacing the alias portion with the portal alias name
                string regexPattern = aliasRegexPattern.Replace("_ALIAS_", escapedAlias);
                //work out how many path separators there are in the portalAlias (ie myalias/mychild = 1 path)
                int pathLength = plainAlias.Split('/').GetUpperBound(0);
                //now work out where in the list we should put this portalAlias regex pattern
                //the list is to be sorted so that those aliases with the most paths 
                //are at the front of the list : ie, they are tested first
                int insertPoint = pathLengths.Count - 1;
                //walk through the existing list of path lengths,
                //and ascertain where in the list this one falls
                //if they are all the same path length, then place them in portal alias order
                for (int i = 0; i < pathLengths.Count; i++)
                {
                    insertPoint = i;
                    if (pathLength > pathLengths[i])
                        //larger than this position, insert at this value
                        break;
                    else
                        insertPoint++; //next one along (if at end, means add)
                }
                if (pathLengths.Count > 0 && insertPoint <= pathLengths.Count - 1)
                {
                    //put the new regex pattern into the correct position
                    regexList.Insert(insertPoint, regexPattern, alias);
                    pathLengths.Insert(insertPoint, pathLength);
                }
                else
                {
                    //put the new regex pattern on the end of the list
                    regexList.Add(regexPattern, alias);
                    pathLengths.Add(pathLength);
                }
            }
            return regexList;
        }
        /// <summary>
        /// Returns a Tab URL based on the settings within the TabInfo
        /// </summary>
        /// <param name="tab"></param>
        /// <returns></returns>
        protected virtual string GetTabUrl(TabInfo ti)
        {
            return GetTabUrl(ti, null);
        }

        protected virtual string GetTabUrl(TabInfo ti, string language, params string[] additionalParameters)
        {
            //we need to check if we have friendly URLs turn off since
            //the URL is not absolute if this is the case, we'll need to
            //complete the full URL ourselves
            string url="";
            
            //friendlyURLs = false; //for testing only
            if (_friendlyURLs)
            {
                if (language == null)
                    url = ti.FullUrl;
                else
                {
                    
                    url = DotNetNuke.Common.Globals.NavigateURL(ti.TabID, false, _portalSettings, "", language, additionalParameters);
                }
                if (url.StartsWith("http://") == false)
                {
                    string[] parts = url.Split('/');
                    string[] aliasParts = _portalAliasURL.Split('/');
                    string prefix = "";
                    int maxAliasPartDepth = 1;
                    if (maxAliasPartDepth > parts.GetUpperBound(0))
                        maxAliasPartDepth = parts.GetUpperBound(0);
                    foreach (string aliasPart in aliasParts)
                    {
                        bool found = false;
                        for (int i = 0; i <= maxAliasPartDepth; i++)
                        {
                            string urlPart = parts[i];
                            if (urlPart.Length > 0)
                            {
                                if (string.Compare(aliasPart, urlPart, true) == 0)
                                {
                                    //this part of hte portal alias is in the url already
                                    found = true;
                                    break;
                                }
                            }
                        }
                        if (found == false)
                        {
                            prefix += aliasPart + "/";
                        }
                        if (maxAliasPartDepth < parts.GetUpperBound(0))
                            maxAliasPartDepth++;
                    }
                    if (url.StartsWith("/"))
                        url = url.TrimStart('/');
                    url = "http://" + prefix + url;
                }
            }
            else 
            {
                url = "http://" + _portalAliasURL + "/Default.aspx?tabid=" + ti.TabID.ToString();
            }
            return url;

        }
        #endregion  

    }
    /// <summary>
    /// The GoogleSiteMapProviderCollection provides a derivation of the ProviderCollection
    /// </summary>
    public class SearchEngineSitemapProviderCol : Dictionary<string, SiteMapProvider>
    {
        public SearchEngineSitemapProviderCol()
            : base()
        {

        }
        public new SiteMapProvider this[string name]
        {
            get
            {
                return (SiteMapProvider)base[name];
            }
        }
        public new void Add(SiteMapProvider provider)
        {
            if (provider == null)
            {
                throw new ArgumentNullException("provider");
            }
            base.Add(provider.Name, provider);
        }
        public SiteMapProvider FindByName(string name)
        {
            if (this.ContainsKey(name))
                return this[name];
            else
                return null;
        }
    }
    /// <summary>
    /// The GoogleSiteMapSection represents the GoogleSiteMapProvider-specific ConfigurationSection within the Config file.
    /// </summary>
    public class GoogleSiteMapSection : ConfigurationSection
    {
        [ConfigurationProperty("providers")]
        public ProviderSettingsCollection Providers
        {
            get { return (ProviderSettingsCollection)base["providers"]; }
        }
        [StringValidator(MinLength = 1)]
        [ConfigurationProperty("defaultProvider",
            DefaultValue = "BaseGoogleSiteMapProvider")]
        public string DefaultProvider
        {
            get { return (string)base["defaultProvider"]; }
            set { base["defaultProvider"] = value; }
        }
    }
}